/*
 * Based on code submited by Hamed_ji  (http://www.codeproject.com/cs/library/Do_Anything_With_ID3.asp)
 */

using System;
using System.Collections.Generic;
using System.IO;

namespace svs.Mobile.BeyondMedia.FeedCore.ID3v2
{
	public class ID3Info
	{
		#region FrameFlags enum

		/// <summary>
		/// Provide frame flags for all frames
		/// </summary>
		[Flags]
		public enum FrameFlags
		{
			TagAlterPreservation = 0x8000,
			FileAlterPreservation = 0x4000,
			ReadOnly = 0x2000,
			Compression = 0x0080,
			Encryption = 0x0040,
			GroupingIdentity = 0x0020
		}

		#endregion

		#region ID3v2Flags enum

		[Flags]
		public enum ID3v2Flags
		{
			Unsynchronisation = 128,
			ExtendedHeader = 64,
			Expremential = 32
		}

		#endregion

		#region TextEncodings enum

		/// <summary>
		/// Provide TextEncoding for frames that support it.
		/// </summary>
		public enum TextEncodings
		{
			Ascii = 0,
			UTF_16 = 1,
			UTF_16BE = 2,
			UTF8 = 3
		}

		#endregion

		private static bool _AutoTextEncoding = true; // when want to save frame use automatic text encoding
		private static TextEncodings _DefaultUnicodeEncoding = TextEncodings.UTF_16;
		// when use AutoTextEncoding which unicode type must use
		private readonly Dictionary<string, TextOnlyFrame> _TextFrames = new Dictionary<string, TextOnlyFrame>();
		private string _FilePath; // ID3 file path
		private ID3v2Flags _Flags;
		private int _ID3BlockSize;
		private bool _HaveTag; // Indicate if current file have ID3v2 Info
		private Version _ver; // Contain ID3 version information
		private Exception m_LastError;
		FileStreamEx _ID3File = null;

		public ID3Info(string FilePath)
		{
			_FilePath = FilePath;
			Load();
		}

		#region -> Public Properties <-

		/// <summary>
		/// Gets FileAddress of current ID3v2
		/// </summary>
		public string FilePath
		{
			get { return _FilePath; }
		}

		/// <summary>
		/// Get FileName of current ID3v2
		/// </summary>
		public string FileName
		{
			get { return Path.GetFileName(_FilePath); }
		}

		/// <summary>
		/// Gets or Sets Flags of current ID3 Tag
		/// </summary>
		public ID3v2Flags Flags
		{
			get { return _Flags; }
			set { _Flags = value; }
		}

		/// <summary>
		/// Get version of current ID3 Tag
		/// </summary>
		public Version VersionInfo
		{
			get { return _ver; }
		}

		/// <summary>
		/// Indicate if current file have ID3v2 Information
		/// </summary>
		public bool HaveTag
		{
			get { return _HaveTag; }
		}

		/// <summary>
		/// Gets or sets unicode encoding of AutoTextEncoding
		/// </summary>
		public static TextEncodings DefaultUnicodeEncoding
		{
			get { return _DefaultUnicodeEncoding; }
			set
			{
				if ((int) value > 3 || (int) value < 2)
					throw (new ArgumentOutOfRangeException("Default unicode must be one of (UTF_16, UTF_16BE, UTF_8)"));

				_DefaultUnicodeEncoding = value;
			}
		}

		/// <summary>
		/// Indicate while saving automatically detect encoding of texts of not
		/// </summary>
		public static bool AutoTextEncoding
		{
			get { return _AutoTextEncoding; }
			set { _AutoTextEncoding = value; }
		}

		public string TagTitle
		{
			get
			{
				if (_TextFrames.ContainsKey("TIT2"))
					return _TextFrames["TIT2"].Text;

				return null;
			}
		}

		#endregion

		private Exception LastError
		{
			get { return m_LastError; }
			set { m_LastError = value; }
		}

		#region -> Private 'Read Methods' <-

		/// <summary>
		/// Load ID3 information from file
		/// </summary>
		/// <exception cref="FileNotFoundException">File Not Found</exception>
		public void Load()
		{
			try
			{
				_ID3File = new FileStreamEx(_FilePath, FileMode.Open);
				if (!_ID3File.HaveID3v2()) // If file don't contain ID3v2 exit function
				{
					_HaveTag = false;
					_ID3File.Close();
					return;
				}

				_HaveTag = true;
				_ver = _ID3File.ReadVersion(); // Read ID3v2 version           
				_Flags = (ID3v2Flags) _ID3File.ReadByte();
				_ID3BlockSize = _ID3File.ReadSize();

				if(_Flags == ID3v2Flags.Unsynchronisation)
				{
					_ID3File.SwitchToResyncronizedStream(_ID3BlockSize);
				}

				ReadFrames();
			}
			catch (Exception ex)
			{
				LastError = ex;
			}
			finally
			{
				if (_ID3File != null)
					_ID3File.Close();
			}
		}

		/// <summary>
		/// Read all frames from specific FileStream
		/// </summary>
		private void ReadFrames()
		{
			// If ID3v2 is ID3v2.2 FrameID, FrameLength of Frames is 3 byte
			// otherwise it's 4 character
			int FrameIDLen = VersionInfo.Minor == 2 ? 3 : 4;

			long Length = _ID3BlockSize;

			// Minimum frame size is 10 because frame header is 10 byte
			while (Length > 10)
			{
				// check for padding( 00 bytes )
				byte Buf = _ID3File.ReadByte();
				if (Buf == 0)
				{
					Length--;
					continue;
				}

				// if readed byte is not zero. it must read as FrameID
				_ID3File.Seek(-1, SeekOrigin.Current);

				// ---------- Read Frame Header -----------------------
				string FrameID = _ID3File.ReadText(FrameIDLen, TextEncodings.Ascii);

				if (FrameIDLen == 3)
					FrameID = FramesInfo.Get4CharID(FrameID);

				int FrameLength = Convert.ToInt32(_ID3File.ReadUInt(FrameIDLen));

				FrameFlags flags;
				if (FrameIDLen == 4)
					flags = (FrameFlags)_ID3File.ReadUInt(2);
				else
					flags = 0; // must set to default flag

				if (Length > 0x10000000)
					throw (new ApplicationException("This file contain frame that have more than 256MB data"));

				bool Added = false;
				if (IsAddable(FrameID)) // Check if frame is not filter
					Added = AddFrame(_ID3File, FrameID, FrameLength, flags);

				if (!Added)
					// if don't read this frame
					// we must go forward to read next frame
					_ID3File.Seek(FrameLength, SeekOrigin.Current);
				else
					return; // we got our frame;

				Length -= FrameLength + 10;
			}
		}

		private bool IsAddable(string id)
		{
			return id == "TIT2";
		}

		/// <summary>
		/// Add Frame information to where it must store
		/// </summary>
		/// <param name="Data">FileStream contain Frame</param>
		/// <param name="FrameID">FrameID of frame</param>
		/// <param name="Length">Maximum available length to read</param>
		/// <param name="Flags">Flags of frame</param>
		private bool AddFrame(FileStreamEx Data, string FrameID, int Length, FrameFlags Flags)
		{
			int IsText = FramesInfo.IsTextFrame(FrameID, _ver.Minor);
			if (IsText == 1)
			{
				TextFrame TempTextFrame = new TextFrame(FrameID, Flags, Data, Length);
				if (TempTextFrame.IsReadableFrame)
				{
					_TextFrames.Add(TempTextFrame.FrameID, TempTextFrame);
					return true;
				}
				return false;
			}

			return false;
		}

		#endregion
	}
}